const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const { PoolStatus } = require("./utils");
const { ZeroAddress } = require("ethers");

describe("DeltaNeutralPortfolio", function () {
  async function deployFixture() {
    const [deployer, user, trader] = await ethers.getSigners();

    const ACLManagerFactory = await ethers.getContractFactory("ACLManager");
    const ACLManager = await ACLManagerFactory.deploy(deployer);
    await ACLManager.deploymentTransaction().wait();

    const registerTx = await ACLManager.connect(deployer).registerTraders([trader]);
    await registerTx.wait();

    const mintAmount = ethers.parseEther("100");
    const TokenFactory = await ethers.getContractFactory("Token");
    const tokenA = await TokenFactory.deploy("TestTokenA", "TTA", mintAmount);
    const tokenB = await TokenFactory.deploy("TestTokenB", "TTB", mintAmount);
    await tokenA.deploymentTransaction().wait();
    await tokenB.deploymentTransaction().wait();

    const DexFactory = await ethers.getContractFactory("FakeDex");
    const dex = await DexFactory.deploy();
    await dex.deploymentTransaction().wait();

    const lendingProfit = ethers.parseEther("1");
    const LendingFactory = await ethers.getContractFactory("FakeLending");
    const lending = await LendingFactory.deploy(lendingProfit);
    await lending.deploymentTransaction().wait();

    const DeltaNeutralPortfolioFactory = await ethers.getContractFactory("DeltaNeutralPortfolio");
    const DNP = await DeltaNeutralPortfolioFactory.deploy(ACLManager, [tokenA, tokenB], [dex], [lending]);
    await DNP.deploymentTransaction().wait();

    const PoolFactory = await ethers.getContractFactory("Pool");
    const pool = await PoolFactory.deploy(ACLManager, tokenA, DNP);
    await pool.deploymentTransaction().wait();

    return { ACLManager, tokenA, tokenB, dex, lending, DNP, pool, deployer, user, trader };
  }

  async function launchPool(DNP, deployer, user, tokenA, pool) {
    // register pool
    const registerPoolTx = await DNP.connect(deployer).registerPool(pool);
    await registerPoolTx.wait();

    // transfer funds to user
    const amount = ethers.parseEther("10");
    const txTransfer = await tokenA.connect(deployer).transfer(user, amount);
    await txTransfer.wait();

    const userBalance = await tokenA.balanceOf(user);
    expect(userBalance).to.equal(amount);

    // deposit funds to the pool
    const txApprove = await tokenA.connect(user).approve(pool, amount);
    await txApprove.wait();
    const txDeposit = await pool.connect(user).deposit(amount);
    await txDeposit.wait();

    const deposit = await pool.deposits(user);
    expect(deposit).to.equal(amount);

    // launch pool
    const launchTx = await pool.connect(deployer).launchPool();
    await launchTx.wait();

    expect(await pool.currentStatus()).to.equal(PoolStatus.LAUNCHED);
    return amount;
  }

  describe("Deployment", function () {
    it("should deploy", async function () {
      const { ACLManager, tokenA, tokenB, dex, DNP, pool } = await loadFixture(deployFixture);

      expect(await DNP.ACLManager()).to.equal(ACLManager);
      expect(await DNP.isAssetAllowed(tokenA)).to.be.true;
      expect(await DNP.isAssetAllowed(tokenB)).to.be.true;
      expect(await DNP.isDexAllowed(dex)).to.be.true;
    });
  });

  describe("registerPool", function () {
    it("should register pool", async function () {
      const { DNP, deployer } = await loadFixture(deployFixture);

      const randPool = ethers.Wallet.createRandom();
      const registerPoolTx = await DNP.connect(deployer).registerPool(randPool);
      await registerPoolTx.wait();

      expect(await DNP.activePool()).to.equal(randPool);
    });

    it("revert if called not by the protocol trader", async function () {
      const { DNP, user } = await loadFixture(deployFixture);

      const randPool = ethers.Wallet.createRandom();
      await expect(DNP.connect(user).registerPool(randPool)).to.be.revertedWith("DeltaNeutralPortfolio: caller is not protocol trader");
    });

    it("revert if pool is zero", async function () {
      const { DNP, deployer } = await loadFixture(deployFixture);

      await expect(DNP.connect(deployer).registerPool(ZeroAddress)).to.be.revertedWith("InvestmentManager: registerPool: pool is zero");
    });

    it("revert if pool is activated", async function () {
      const { DNP, deployer } = await loadFixture(deployFixture);

      const randPool = ethers.Wallet.createRandom();
      const registerPoolTx = await DNP.connect(deployer).registerPool(randPool);
      await registerPoolTx.wait();

      expect(await DNP.activePool()).to.equal(randPool);

      await expect(DNP.connect(deployer).registerPool(randPool)).to.be.revertedWith(
        "InvestmentManager: registerPool: pool is already activated",
      );
    });
  });

  describe("entry", function () {
    it("entry should transferFrom pool all tokens", async function () {
      const { tokenA, DNP, pool, deployer, user } = await loadFixture(deployFixture);
      const depositAmount = await launchPool(DNP, deployer, user, tokenA, pool);

      // check DeltaNeutralPortfolio balance
      expect(await tokenA.balanceOf(DNP)).to.equal(depositAmount);
    });

    it("revert if called not by the pool", async function () {
      const { DNP, deployer } = await loadFixture(deployFixture);

      await expect(DNP.connect(deployer).entry()).to.be.revertedWith("InvestmentManager: caller is not the active pool");
    });
  });

  describe("exit", function () {
    it("exit should transfer to the pool all tokens", async function () {
      const { tokenA, DNP, pool, deployer, user, trader } = await loadFixture(deployFixture);
      // check DNP balance before
      expect(await tokenA.balanceOf(DNP)).to.equal(0);

      const depositAmount = await launchPool(DNP, deployer, user, tokenA, pool);
      // check DNP balance after launch
      expect(await tokenA.balanceOf(DNP)).to.equal(depositAmount);

      const finishTx = await pool.connect(trader).finishPool();
      await finishTx.wait();
      // check DNP balance after exit
      expect(await tokenA.balanceOf(DNP)).to.equal(0);
    });

    it("revert if called not by the pool", async function () {
      const { DNP, user } = await loadFixture(deployFixture);

      await expect(DNP.connect(user).exit()).to.be.revertedWith("InvestmentManager: caller is not the active pool");
    });
  });

  describe("SwapManager", function () {
    it("should swap tokens", async function () {
      const { tokenA, tokenB, DNP, pool, deployer, user, trader, dex } = await loadFixture(deployFixture);
      const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);
      const tokenBAmount = tokenAAmount * 2n;

      const swapTx = await DNP.connect(trader).swap(dex, tokenA, tokenB, tokenAAmount, tokenBAmount);
      await swapTx.wait();

      // check tokenB balance
      expect(await tokenB.balanceOf(DNP)).to.equal(tokenBAmount);
    });

    it("revert if called not by the trader", async function () {
      const { tokenA, tokenB, DNP, pool, deployer, user, dex } = await loadFixture(deployFixture);
      const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);
      const tokenBAmount = tokenAAmount * 2n;

      await expect(DNP.connect(user).swap(dex, tokenA, tokenB, tokenAAmount, tokenBAmount)).to.be.revertedWith(
        "DeltaNeutralPortfolio: caller is not protocol trader",
      );
    });

    it("revert if dex is not allowed", async function () {
      const { tokenA, tokenB, DNP, pool, deployer, user, trader, dex } = await loadFixture(deployFixture);
      const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);
      const tokenBAmount = tokenAAmount * 2n;

      const randDex = ethers.Wallet.createRandom();
      await expect(DNP.connect(trader).swap(randDex, tokenA, tokenB, tokenAAmount, tokenBAmount)).to.be.revertedWith(
        "SwapManager: swap: dex is not allowed",
      );
    });

    it("revert if tokenIn is not allowed", async function () {
      const { tokenA, tokenB, DNP, pool, deployer, user, dex, trader } = await loadFixture(deployFixture);
      const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);
      const tokenBAmount = tokenAAmount * 2n;

      const randTokenA = ethers.Wallet.createRandom();
      await expect(DNP.connect(trader).swap(dex, randTokenA, tokenB, tokenAAmount, tokenBAmount)).to.be.revertedWith(
        "SwapManager: swap: tokenIn is not allowed",
      );
    });

    it("revert if tokenOut is not allowed", async function () {
      const { tokenA, DNP, pool, deployer, user, dex, trader } = await loadFixture(deployFixture);
      const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);
      const tokenBAmount = tokenAAmount * 2n;

      const randTokenB = ethers.Wallet.createRandom();
      await expect(DNP.connect(trader).swap(dex, tokenA, randTokenB, tokenAAmount, tokenBAmount)).to.be.revertedWith(
        "SwapManager: swap: tokenOut is not allowed",
      );
    });

    describe("LendingManager", function () {
      it("should lend and withdraw token", async function () {
        const { tokenA, DNP, pool, deployer, user, trader, lending } = await loadFixture(deployFixture);
        const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);

        const depositTx = await DNP.connect(trader).deposit(lending, tokenA, tokenAAmount);
        await depositTx.wait();

        // check tokenA balance
        expect(await tokenA.balanceOf(lending)).to.equal(tokenAAmount);

        const withdrawTx = await DNP.connect(trader).withdraw(lending, tokenA, tokenAAmount);
        await withdrawTx.wait();

        // check tokenA balance
        expect(await tokenA.balanceOf(DNP)).to.equal(tokenAAmount + (await lending.profit()));
      });

      it("revert if called not by the trader", async function () {
        const { tokenA, DNP, pool, deployer, user, lending } = await loadFixture(deployFixture);
        const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);

        await expect(DNP.connect(user).deposit(lending, tokenA, tokenAAmount)).to.be.revertedWith(
          "DeltaNeutralPortfolio: caller is not protocol trader",
        );
        await expect(DNP.connect(user).withdraw(lending, tokenA, tokenAAmount)).to.be.revertedWith(
          "DeltaNeutralPortfolio: caller is not protocol trader",
        );
      });

      it("revert if lending is not allowed", async function () {
        const { tokenA, DNP, pool, deployer, user, trader } = await loadFixture(deployFixture);
        const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);

        const randLending = ethers.Wallet.createRandom();
        await expect(DNP.connect(trader).deposit(randLending, tokenA, tokenAAmount)).to.be.revertedWith(
          "LendingManager: deposit: lending is not allowed",
        );
        await expect(DNP.connect(trader).withdraw(randLending, tokenA, tokenAAmount)).to.be.revertedWith(
          "LendingManager: withdraw: lending is not allowed",
        );
      });

      it("revert if token is not allowed", async function () {
        const { tokenA, DNP, pool, deployer, user, trader, lending } = await loadFixture(deployFixture);
        const tokenAAmount = await launchPool(DNP, deployer, user, tokenA, pool);

        const randTokenA = ethers.Wallet.createRandom();
        await expect(DNP.connect(trader).deposit(lending, randTokenA, tokenAAmount)).to.be.revertedWith(
          "LendingManager: deposit: token is not allowed",
        );
        await expect(DNP.connect(trader).withdraw(lending, randTokenA, tokenAAmount)).to.be.revertedWith(
          "LendingManager: withdraw: token is not allowed",
        );
      });
    });
  });
});
